web worker
web worker的目的就是解放主线程,Web Worker 是脱离在主线程之外的,将一些复杂的耗时的活交给它干,完成后通过 postMessage 方法告诉主线程,而主线程通过 onMessage 方法得到 Web Worker 的结果反馈。
但 Web Worker 是临时的,每次做的事情的结果还不能被持久存下来,如果下次有同样的复杂操作,还得费时间的重新来一遍。
service worker 在web worker的基础上加上了持久离线缓存能力。在 Service Worker 之前也有在 HTML5 上做离线缓存的 API 叫 AppCache, 但是 AppCache 存在很多 不能忍受的缺点
Service Worker
Service Worker可以拦截页面向服务端发出的网络请求,然后根据一些条件 来判断请求本地的缓存还是云端的服务,再把请求到的内容放到本地的缓存中来管理。
service worker特性
- 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
- 拦截和处理网络请求,操作缓存
可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
- 支持push API等
- 后台同步 & 更新缓存
- 一旦被 install,就永远存在,除非被手动 unregister
- 用到的时候可以直接唤醒,不用的时候自动睡眠
- 离线内容开发者可控
- 能向客户端推送消息
- 不能直接操作 DOM
- 必须在 HTTPS 环境下才能工作
- 异步实现,内部大都是通过 Promise 实现
1 | page ——service worker —— server |
基本工作步骤
Service Worker 的工作原理是基于注册、安装、激活等步骤在浏览器 js 主线程中独立分担缓存任务的。
- 首先我们需要在页面的 JavaScript 主线程中使用 navigator.serviceWorker.register() 来注册 Service Worker ,在注册的过程中,浏览器会在后台启动尝试 Service Worker 的安装步骤。
- 如果注册成功,Service Worker 在 ServiceWorkerGlobalScope 环境中运行; 这是一个特殊的 worker context,与主脚本的运行线程相独立,同时也没有访问 DOM 的能力。
- 后台开始安装步骤,通常在安装的过程中需要缓存一些静态资源。如果所有的资源成功缓存则安装成功,如果有任何静态资源缓存失败则安装失败,在这里失败的不要紧,会自动继续安装直到安装成功,如果安装不成功无法进行下一步 — 激活 Service Worker。
- 开始激活 Service Worker,必须要在 Service Worker 安装成功之后,才能开始激活步骤,当 Service Worker 安装完成后,会接收到一个激活事件(activate event)。激活事件的处理函数中,主要操作是清理旧版本的 Service Worker 脚本中使用资源。
- 激活成功后 Service Worker 可以控制页面了,但是只针对在成功注册了 Service Worker后打开的页面。也就是说,页面打开时有没有 Service Worker,决定了接下来页面的生命周期内受不受 Service Worker 控制。所以,只有当页面刷新后,之前不受 Service Worker 控制的页面才有可能被控制起来。
service worker 生命周期
在页面脚本中注册 Service Worker 文件所在的 URL。Worker 就可以开始激活了,激活后的 Service Worker 可以监听当前域下的功能性事件,比如资源请求(fetch)、推送通知(push)、后台同步(sync)。在这一系列的流程中,从 Service Worker 的注册到消失,经历了生命周期中不同的状态。
了解一下 Service Worker 的生命周期的概念,这有利于我们学会在各个生命周期的阶段进行有目的性的回调,让我们自定义的工作在 Service Worker 中正确有效的开展下去。
1. 安装中(installing)
这个状态发生在 Service Worker 注册之后,表示开始安装,触发 install 事件回调指定一些静态资源进行离线缓存。
- install 事件回调中有两个方法:
- event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止
- self.skipWaiting():self 是当前 context 的 global 变量,执行该方法表示强制当前处在 waiting 状态的 Service Worker 进入 activate 状态。
2. 安装后( installed )
Service Worker 已经完成了安装,并且等待其他的 Service Worker 线程被关闭。
3. 激活( activating )
在这个状态下没有被其他的 Service Worker 控制的客户端,允许当前的 worker 完成安装,并且清除了其他的 worker 以及关联缓存的旧缓存资源,等待新的 Service Worker 线程被激活。
- activate 回调中有两个方法:
- event.waitUntil():传入一个 Promise 为参数,等到该 Promise 为 resolve 状态为止。
- self.clients.claim():在 activate 事件回调中执行该方法表示取得页面的控制权, 这样之后打开页面都会使用版本更新的缓存。旧的 Service Worker 脚本不再控制着页面,之后会被停止。
4. 激活后( activated )
在这个状态会处理 activate 事件回调 (提供了更新缓存策略的机会)。并可以处理功能性的事件 fetch (请求)、sync (后台同步)、push (推送)
5. 废弃状态 ( redundant )
这个状态表示一个 Service Worker 的生命周期结束。
- 进入废弃 (redundant) 状态的原因可能为这几种
- 安装 (install) 失败
- 激活 (activating) 失败
- 新版本的 Service Worker 替换了它并成为激活状态
支持的事件
- install:Service Worker 安装成功后被触发的事件,在事件处理函数中可以添加需要缓存的文件
- activate:当 Service Worker 安装完成后并进入激活状态,会触发 activate 事件。通过监听 activate 事件你可以做一些预处理,如对旧版本的更新、对无用缓存的清理等
- message:Service Worker 运行于独立 context 中,无法直接访问当前页面主线程的 DOM 等信息,但是通过 postMessage API,可以实现他们之间的消息传递,这样主线程就可以接受 Service Worker 的指令操作 DOM。
Service Worker 有几个重要的功能性的的事件,这些功能性的事件支撑和实现了 Service Worker 的特性。
- fetch (请求):当浏览器在当前指定的 scope 下发起请求时,会触发 fetch 事件,并得到传有 response 参数的回调函数,回调中就可以做各种代理缓存的事情了。
- push (推送):push 事件是为推送准备的。通过 PUSH API,当订阅了推送服务后,可以使用推送方式唤醒 Service Worker 以响应来自系统消息传递服务的消息,即使用户已经关闭了页面。
- sync 事件由 background sync (后台同步)发出。background sync 配合 Service Worker 推出的 API,用于为 Service Worker 提供一个可以实现注册和监听同步处理的方法。但它还不在 W3C Web API 标准中。在 Chrome 中这也只是一个实验性功能,需要访问 chrome://flags/#enable-experimental-web-platform-features ,开启该功能,然后重启生效。
使用service worker
前提条件
Service Worker 出于安全性和其实现原理,在使用的时候有一定的前提条件。
- 由于 Service Worker 要求 HTTPS 的环境
- Service Worker 的缓存机制是依赖 Caches/Cache API 实现的
- 依赖 HTML5 fetch API
- 依赖 Promise 实现
注册
要安装 Service Worker, 我们需要通过在 js 主线程(常规的页面里的 js )注册 Service Worker 来启动安装,这个过程将会通知浏览器我们的 Service Worker 线程的 javaScript 文件在什么地方呆着。
1 | // 判断是否支持service worker |
- 如果浏览器支持service worker的话,在页面onload 的时候注册位于 /sw.js 的 Service Worker。
- 每次页面加载成功后,就会调用 register() 方法,浏览器将会判断 Service Worker 线程是否已注册并做出相应的处理。
- register 方法的 scope 参数是可选的,用于指定你想让 Service Worker 控制的内容的子目录。’/‘:服务工作线程文件位于根网域, 这意味着服务工作线程的作用域将是整个来源。
Service Worker 线程将接收 scope 指定网域目录上所有事项的 fetch 事件,如果我们的 Service Worker 的 javaScript 文件在 /a/b/sw.js, 不传 scope 值的情况下, scope 的值就是 /a/b。
scope 的值的意义在于,如果 scope 的值为 /a/b, 那么 Service Worker 线程只能捕获到 path 为 /a/b 开头的( /a/b/page1, /a/b/page2,…)页面的 fetch 事件。通过 scope 的意义我们也能看出 Service Worker 不是服务单个页面的,所以在 Service Worker 的 js 逻辑中全局变量需要慎用。
安装
在你的 Service Worker 注册成功之后呢,我们的浏览器中已经有了一个属于你自己 web App 的 worker context 啦, 在此时,浏览器就会马不停蹄的尝试为你的站点里面的页面安装并激活它,并且在这里可以把静态资源的缓存给办了。
install 事件我们会绑定在 Service Worker 文件中,在 Service Worker 安装成功后,install 事件被触发。
install 事件一般是被用来填充你的浏览器的离线缓存能力。为了达成这个目的,我们使用了 Service Worker 新的标志性的存储 cache API — 一个 Service Worker 上的全局对象,它使我们可以存储网络响应发来的资源,并且根据它们的请求来生成key。这个 API 和浏览器的标准的缓存工作原理很相似,但是是只对应你的站点的域的。它会一直持久存在,直到你告诉它不再存储,你拥有全部的控制权。
localStorage 的用法和 Service Worker cache 的用法很相似,但是由于 localStorage 是同步的用法,所以不允许在 Service Worker 中使用。 IndexedDB 也可以在 Service Worker 内做数据存储。
1 | // 监听 service worker 的 install 事件 |
- event-waitUntil()方法确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成
- 在 waitUntil() 内,我们使用了 caches.open() 方法来创建了一个叫做 v1 的新的缓存,将会是我们的站点资源缓存的第一个版本。它返回了一个创建缓存的 promise,当它 resolved 的时候,我们接着会调用在创建的缓存实例(Cache API)上的一个方法 addAll(),这个方法的参数是一个由一组相对于 origin 的 URL 组成的数组,这些 URL 就是你想缓存的资源的列表。
- 如果 promise 被 rejected,安装就会失败,这个 worker 不会做任何事情。这也是可以的,因为你可以修复你的代码,在下次注册发生的时候,又可以进行尝试。
- 当安装成功完成之后,Service Worker 就会激活。在第一次你的 Service Worker 注册/激活时,这并不会有什么不同。但是当 Service Worker 更新的时候 ,就不太一样了。
自定义请求响应
走到这一步,其实现在你已经可以将你的站点资源缓存了,你需要告诉 Service Worker 让它用这些缓存内容来做点什么。有了 fetch 事件,这是很容易做到的。
每次任何被 Service Worker 控制的资源被请求到时,都会触发 fetch 事件,这些资源包括了指定的 scope 内的 html 文档,和这些 html 文档内引用的其他任何资源(比如 index.html 发起了一个跨域的请求来嵌入一个图片,这个也会通过 Service Worker),这下 Service Worker 代理服务器的形象开始慢慢露出来了,而这个代理服务器的钩子就是凭借 scope 和 fetch 事件两大利器就能把站点的请求管理的井井有条。
可以给 Service Worker 添加一个 fetch 的事件监听器,接着调用 event 上的 respondWith() 方法来劫持我们的 HTTP 响应
1 | this.addEventListener('fetch', function (event) { |
我们可以在 install 的时候进行静态资源缓存,也可以通过 fetch 事件处理回调来代理页面请求从而实现动态资源缓存。两种方式的区别:
- on install 的优点是第二次访问即可离线,缺点是需要将需要缓存的 URL 在编译时插入到脚本中,增加代码量和降低可维护性;
- on fetch 的优点是无需更改编译过程,也不会产生额外的流量,缺点是需要多一次访问才能离线可用。
除了静态的页面和文件之外,如果对 Ajax 数据加以适当的缓存可以实现真正的离线可用, 要达到这一步可能需要对既有的 Web App 进行一些重构以分离数据和模板。
service worker版本更新
/sw.js 控制着页面资源和请求的缓存,那么如果缓存策略需要更新呢?也就是如果 /sw.js 有更新怎么办?/sw.js 自身该如何更新?
如果 /sw.js 内容有更新,当访问网站页面时浏览器获取了新的文件,逐字节比对 /sw.js 文件发现不同时它会认为有更新启动 更新算法,于是会安装新的文件并触发 install 事件。但是此时已经处于激活状态的旧的 Service Worker 还在运行,新的 Service Worker 完成安装后会进入 waiting 状态。直到所有已打开的页面都关闭,旧的 Service Worker 自动停止,新的 Service Worker 才会在接下来重新打开的页面里生效。
自动更新所有页面
如果希望在有了新版本时,所有的页面都得到及时自动更新怎么办呢?可以在 install 事件中执行 self.skipWaiting() 方法跳过 waiting 状态,然后会直接进入 activate 阶段。接着在 activate 事件发生时,通过执行 self.clients.claim() 方法,更新所有客户端上的 Service Worker。
1 | // 安装阶段跳过等待,直接进入 active |
另外要注意一点,/sw.js 文件可能会因为浏览器缓存问题,当文件有了变化时,浏览器里还是旧的文件。这会导致更新得不到响应。如遇到该问题,可尝试这么做:在 Web Server 上添加对该文件的过滤规则,不缓存或设置较短的有效期。
手动更新service worker
其实在页面中,也可以手动借助 Registration.update() 更新。
1 | var version = '1.0.1'; |
debug 时更新
Service Worker 被载入后立即激活可以保证每次 /sw.js 为最新的。1
2
3self.addEventListener('install', function () {
self.skipWaiting();
});
Service Worker 的特殊之处除了由浏览器触发更新之外,还应用了特殊的缓存策略: 如果该文件已 24 小时没有更新,当 Update 触发时会强制更新。这意味着最坏情况下 Service Worker 会每天更新一次。